#include "socket_address.h"

#include <arpa/inet.h>
#include <cstdint>
#include <cstring>
#include <map>
#include <netdb.h>
#include <netinet/in.h>
#include <string>
#include <sys/socket.h>
#include <vector>
#include <unistd.h>
#include <mutex>
#include <sys/time.h>

uint64_t GetTimeOfSecond()
{
    struct timeval tm = {};
    gettimeofday(&tm, nullptr);
    return tm.tv_sec + tm.tv_usec / 1000 / 1000;
}

struct Addr
{
    std::vector<SocketAddress> m_addr;
    uint64_t                   m_expire_time{};
};

struct DomainCache
{
    std::map<std::string, Addr> m_addr;
    std::mutex m_mtx;
};

static DomainCache domain_cache;

bool GetCache(const std::string & domain, const std::string & protocol, std::vector<SocketAddress> & address_vec)
{
    std::unique_lock<std::mutex> lock(domain_cache.m_mtx);
    std::string key = domain + protocol;
    auto iter = domain_cache.m_addr.find(key);
    if (iter != domain_cache.m_addr.end())
    {
        if (iter->second.m_expire_time > GetTimeOfSecond())
        {
            domain_cache.m_addr.erase(iter);
            return false;
        }
        address_vec = iter->second.m_addr;
        return true;
    }
    return false;
}

bool DomainCache(const std::string & domain, const std::string & protocol, std::vector<SocketAddress> & address_vec)
{
    std::unique_lock<std::mutex> lock(domain_cache.m_mtx);
    std::string key = domain + protocol;
    auto iter = domain_cache.m_addr.find(key);
    if (iter != domain_cache.m_addr.end())
    {
        iter->second.m_addr = address_vec;
        iter->second.m_expire_time = GetTimeOfSecond() + 100;
        return true;
    }
    else
    {
        domain_cache.m_addr[key].m_addr = address_vec;
        domain_cache.m_addr[key].m_expire_time = GetTimeOfSecond() + 100;
    }
    return true;
}

SocketAddress SocketAddress::Address(const struct sockaddr_in & addr)
{
    SocketAddress socket_address = {};
    socket_address.m_sai = addr;
    return socket_address;
}

SocketAddress SocketAddress::Address(const std::string & ip, uint16_t port)
{
    SocketAddress address    = {};
    address.m_sai.sin_family = AF_INET;
    address.m_sai.sin_port   = htons(port);
    inet_pton(address.m_sai.sin_family, ip.c_str(), &address.m_sai.sin_addr);
    return address;
}

SocketAddress SocketAddress::Address(const std::string & key)
{
    auto index = key.find(':');
    if (index != std::string::npos)
    {
        std::string ip   = key.substr(0, index);
        uint16_t    port = atoi(key.substr(index + 1).c_str());
        return Address(ip, port);
    }
    return SocketAddress{};
}

int SocketAddress::Address(
    const std::string & domain,
    const std::string & protocol,
    std::vector<SocketAddress> & address_vec)
{
    if (GetCache(domain, protocol, address_vec))
    {
        return 1;
    }
    struct ::addrinfo   hint        = {};
    struct ::addrinfo * result_addr = nullptr;
    hint.ai_family                = AF_INET;
    hint.ai_flags                 = 0;
    hint.ai_socktype              = SOCK_STREAM;
    hint.ai_protocol              = IPPROTO_TCP;
    int result                    = ::getaddrinfo(domain.c_str(), protocol.c_str(), &hint, &result_addr);
    if (result < 0)
    {
        result = -1;
    }
    else
    {
        result = 1;
        for (auto ptr = result_addr; ptr != nullptr; ptr = ptr->ai_next)
        {
            if (ptr->ai_addrlen == sizeof(struct ::sockaddr_in))
            {
                SocketAddress address;
                address.m_sai = *(struct ::sockaddr_in *)ptr->ai_addr;
                address_vec.emplace_back(address);
            }
            else if (ptr->ai_addrlen == sizeof(struct sockaddr_in6))
            {
            }
        }
        result = address_vec.empty() ? -1 : 1;
        if (result == 1)
        {
            DomainCache(domain, protocol, address_vec);
        }
    }
    return result;
}

int SocketAddress::UdpAddress(const std::string & domain, uint16_t port, std::vector<SocketAddress> & address_vec)
{
    struct ::addrinfo   hint        = {};
    struct ::addrinfo * result_addr = nullptr;
    hint.ai_family                = AF_INET;
    hint.ai_flags                 = 0;
    hint.ai_socktype              = SOCK_DGRAM;
    hint.ai_protocol              = IPPROTO_UDP;
    int result                    = ::getaddrinfo(domain.c_str(), std::to_string(port).c_str(), &hint, &result_addr);
    if (result < 0)
    {
        printf("%s.\n", strerror(errno));
        result = -1;
    }
    else
    {
        result = 1;
        for (auto ptr = result_addr; ptr != nullptr; ptr = ptr->ai_next)
        {
            if (ptr->ai_addrlen == sizeof(struct ::sockaddr_in))
            {
                SocketAddress address;
                address.m_sai = *(struct ::sockaddr_in *)ptr->ai_addr;
                address_vec.emplace_back(address);
            }
            else if (ptr->ai_addrlen == sizeof(struct sockaddr_in6))
            {
            }
        }
        result = address_vec.empty() ? -1 : 1;
    }
    return result;
}

int SocketAddress::Address(const std::string & domain, uint16_t port, std::vector<SocketAddress> & address_vec)
{
    int result = Address(domain, std::to_string(port), address_vec);
    return result;
}

SocketAddress::SocketAddress()
{
    memset(&m_sai, 0, sizeof(m_sai));
}

struct sockaddr_in SocketAddress::NetAddress() const
{
    return m_sai;
}

std::string SocketAddress::AddressToString() const
{
    char address[128] = {};
    inet_ntop(AF_INET, &m_sai.sin_addr.s_addr, address, sizeof(address));
    std::string addr = address;
    return addr;
}

uint16_t SocketAddress::PortToNumber() const
{
    uint16_t port = ntohs(m_sai.sin_port);
    return port;
}

void SocketAddress::SockAddr(struct sockaddr_in & addr)
{
    m_sai = addr;
}

void SocketAddress::PortNumber(uint16_t port)
{
    m_sai.sin_port = htons(port);
}

void SocketAddress::HostName(const std::string & host_name)
{
    m_host_name = host_name;
}

std::string SocketAddress::HostName() const
{
    return m_host_name;
}

std::string SocketAddress::AddressWithPortKey() const
{
    std::string key = AddressToString() + ":" + std::to_string(PortToNumber());
    return key;
}

